﻿<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>CodeProject.AI Explorer</title>

    <!-- If you use this, ensure the body has class "dark-mode"-->
    <link rel="stylesheet" type="text/css" href="assets/bootstrap.min.css">
    <!-- for light mode should you wish it
       <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" />
    -->
    <!-- Will only be effective if you're connected to the internet -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@200;400;600&display=swap" rel="stylesheet">

    <link rel="icon" type="image/png" sizes="32x32" href="assets/favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="16x16" href="assets/favicon-16x16.png">
    <style>
        .header {
            font-family: Inter, Arial, Helvetica, Arial, sans-serif;
            padding: 10px 0;
            margin-bottom: 25px;
        }

        .nav-link {
            padding: 0.5rem 1rem;
        }

        .site-title {
            text-decoration: none !important;
            font-size: 2.7rem;
            display: inline-block;
            text-transform: uppercase;
            vertical-align: middle;
            letter-spacing: -3px;
        }

            .site-title .front {
                font-weight: 200;
            }

            .site-title .end {
                font-weight: 600;
            }

        #faces::file-selector-button {
            width: 10rem;
        }

        input[type="range"]::-webkit-slider-thumb {
            margin-top: -7px;
        }


        .dark-mode .header {
            background: rgb(255,153,0);
        }

        .dark-mode .card-body {
            background-color: rgb(108,117,125);
        }

        .dark-mode .site-title {
            color: white;
        }

        .dark-mode .nav-tabs {
            background-color: #4b4b4b;
        }

            .dark-mode .nav-tabs .nav-link.active {
                background-color: #808080;
            }

        .dark-mode input[type=range]::-webkit-slider-runnable-track {
            background-color: lightgray;
            height: 2px;
        }

        .dark-mode input[type="range"] {
            background-color: grey;
        }
    </style>

    <script type="text/javascript">

        const useSVG = true;
        const pingFrequency = 1000; // milliseconds
        const serviceTimeoutSec = 10;   // seconds

        const apiServiceProtocol = window.location.protocol;
        const apiServiceHostname = window.location.hostname || "localhost";
        const apiServicePort = window.location.port === "" ? "" : ":" + (window.location.port || 32168);
        const apiServiceUrl = `${apiServiceProtocol}//${apiServiceHostname}${apiServicePort}`;

        let serverOnline = false;

        window.addEventListener('DOMContentLoaded', function (event) {
            serviceUrl.value = apiServiceUrl;
            serviceTimeoutSecTxt.value = serviceTimeoutSec;
            setStatus("Searching for API Server...", "orange");
            setInterval(ping, pingFrequency);
            setInterval(getAnalysisStatus, pingFrequency);
            setTimeout(setCustomModelLists, 5000);
        });

        function adjustForImageSize() {

            if (useSVG) {
                // clearCanvas();
                imageMask.style.height = this.height + 'px';
                imageMask.style.width = this.width + 'px';
            }
            else {
                annotations.height = this.height;
                annotations.width = this.width;
            }

            return true;
        }

        function previewImage(fileChooser) {

            if (fileChooser.files)
                setImage(fileChooser.files[0]);
        }

        function setImage(imageToSet) {

            clearCanvas();
            img.onload = adjustForImageSize;
            img.src = URL.createObjectURL(imageToSet);
            img.style.height = "auto";
            img.style.visibility = "visible";
        }

        function clearCanvas() {
            if (useSVG) {
                imageMask.innerHTML = '';
                img.style.visibility = "hidden";
            }
            else {
                var context = annotations.getContext("2d");
                context.clearRect(0, 0, annotations.width, annotations.height);
            }
        }

        function setError(message) {
            setStatus(message, "red")
            console.log(message);
        }

        function setStatus(text, color) {
            if (color)
                document.getElementById("status").innerHTML = "<span style='color:" + color + "'>" + text + "</span>";
            else
                document.getElementById("status").innerHTML = "<span>" + text + "</span>";
        }

        function drawRectangles(predictions) {

            if (!img.width || !img.height)
                return;

            if (!(predictions && predictions.length > 0))
                return;

            // Sort descending
            predictions.sort(function (a, b) {
                return b.confidence - a.confidence;
            });

            let svg = null;
            let xRatio = img.width * 1.0 / img.naturalWidth;
            let yRatio = img.height * 1.0 / img.naturalHeight;

            if (useSVG) {
                svg = `
                                                                <svg viewBox="0 0 ${img.width} ${img.height}">
                                                                <defs>
                                                                    <mask id="mask">
                                                                        <rect fill="#999" x="0" y="0" width="${img.width}" height="${img.height}"></rect>`;
            }
            else {
                let context = annotations.getContext("2d");
                context.clearRect(0, 0, annotations.width, annotations.height);
                context.lineWidth = "1";
            }

            for (var i = 0; i < predictions.length; i++) {
                let rect = predictions[i];
                let left = Math.min(rect.x_min, rect.x_max) * xRatio;
                let top = Math.min(rect.y_min, rect.y_max) * yRatio;
                let width = Math.abs(rect.x_min - rect.x_max) * xRatio;
                let height = Math.abs(rect.y_min - rect.y_max) * yRatio;

                if (useSVG) {
                    svg += `<rect fill="#ffffff" x="${left}" y="${top}" width="${width}" height="${height}"></rect>`;
                }
                else {
                    context.strokeStyle = "red";
                    context.strokeRect(left, top, width, height);
                    context.strokeStyle = "yellow";
                    context.strokeText(i + " " + (rect.label || ""), left, top);
                }
            }

            if (useSVG) {
                svg += `
                                                                    </mask>
                                                                </defs>
                                                                <image mask="url(#mask)" xmlns:xlink="http://www.w3.org/1999/xlink"
                                                                       xlink:href="${img.src}" width="${img.width}" height="${img.height}"></image>`;

                let colors = ["179,221,202", "204,223,120", "164,221,239"];
                let colorIndex = 0;

                let maxLineWidth = predictions.length > 5 ? (predictions.length > 10 ? 2 : 5) : 15;
                for (var i = 0; i < predictions.length; i++) {

                    let rect = predictions[i];
                    let left = Math.min(rect.x_min, rect.x_max) * xRatio;
                    let top = Math.min(rect.y_min, rect.y_max) * yRatio;
                    let width = Math.abs(rect.x_min - rect.x_max) * xRatio;
                    let height = Math.abs(rect.y_min - rect.y_max) * yRatio;

                    let right = left + width;
                    let bottom = top + height;

                    // CodeProject.AI style
                    let lineWidth = Math.min(maxLineWidth, width / 10);
                    let blockWidth = (width - lineWidth) / 5;
                    let blockHeight = (height - lineWidth) / 5;
                    let color = colors[colorIndex++ % colors.length];
                    let styleSolid = `style="stroke:rgba(${color}, 1);stroke-width:${lineWidth}"`;
                    let styleTrans = `style="stroke:rgba(${color}, 0.5);stroke-width:${lineWidth}"`;

                    // label
                    let label = rect.label || rect.userid;
                    if (label)
                        svg += `<text x="${left}" y="${top - lineWidth}" style="stroke: none; fill:rgba(${color}, 1);font-size:12px">${label || ""}</text>`;

                    // Shortcut if there are just too many items
                    if (predictions.length > 15) {
                        // Solid rectangle
                        svg += `<rect stroke="rgb(${color})" stroke-width="1px" fill="transparent" x="${left}" y="${top}" width="${width}" height="${height}"></rect>`;
                        continue;
                    }

                    // Top (left to right)
                    let x = left - lineWidth / 2;
                    svg += `<line ${styleSolid} x1="${x}" y1="${top}" x2="${x + blockWidth + lineWidth}" y2="${top}"/>`;
                    x += blockWidth + lineWidth;
                    svg += `<line ${styleTrans} x1="${x}" y1="${top}" x2="${x + blockWidth}" y2="${top}"/>`;
                    x += 2 * blockWidth; // skip a section
                    svg += `<line ${styleSolid} x1="${x}" y1="${top}" x2="${x + 2 * blockWidth + lineWidth}" y2="${top}"/>`;

                    // Right (top to bottom)
                    let y = top - lineWidth / 2;
                    svg += `<line ${styleSolid} x1="${right}" y1="${y}" x2="${right}" y2="${y + blockHeight + lineWidth}"/>`;
                    y += blockHeight + lineWidth;
                    svg += `<line ${styleTrans}  x1="${right}" y1="${y}" x2="${right}" y2="${y + blockHeight}"/>`;
                    y += 2 * blockHeight; // skip a section
                    svg += `<line ${styleSolid} x1="${right}" y1="${y}" x2="${right}" y2="${y + 2 * blockHeight + lineWidth}"/>`;

                    // Bottom (left to right)
                    x = left - lineWidth / 2;
                    svg += `<line ${styleSolid} x1="${x}" y1="${bottom}" x2="${x + 2 * blockWidth + lineWidth}" y2="${bottom}"/>`;
                    x += 3 * blockWidth + lineWidth; // Skip a section
                    svg += `<line ${styleTrans}  x1="${x}" y1="${bottom}" x2="${x + blockWidth}" y2="${bottom}"/>`;
                    x += blockWidth;
                    svg += `<line ${styleSolid} x1="${x}" y1="${bottom}" x2="${x + blockWidth + lineWidth}" y2="${bottom}"/>`;

                    // Left (top to bottom)
                    y = top - lineWidth / 2;
                    svg += `<line ${styleSolid} x1="${left}" y1="${y}" x2="${left}" y2="${y + 2 * blockHeight + lineWidth}"/>`;
                    y += 3 * blockHeight + lineWidth; // skip a section
                    svg += `<line ${styleTrans}  x1="${left}" y1="${y}" x2="${left}" y2="${y + blockHeight}"/>`;
                    y += blockHeight;
                    svg += `<line ${styleSolid} x1="${left}" y1="${y}" x2="${left}" y2="${y + blockHeight + lineWidth}"/>`;
                }

                svg += `
                                                            </svg>`;

                imageMask.innerHTML = svg;
                img.style.visibility = "hidden";
            }
        }

        function displayPredictionResults(predictions) {

            if (!(predictions && predictions.length > 0)) {
                result.innerHTML = "No predictions returned";
                return;
            }

            // Sort descending
            predictions.sort(function (a, b) {
                return b.confidence - a.confidence;
            });

            var html = "<table style='width:100%'><tr><th>#</th><th>Label</th><th>Confidence</th></tr>";
            for (var i = 0; i < predictions.length; i++) {
                var pred = predictions[i];
                html += "<tr><td>" + i + "</td><td>" + (pred.label || "Face")
                    + "</td><td>" + confidence(pred.confidence) + "</td></tr>";
            }
            html += "</table>";
            result.innerHTML = html;
        }

        function displayRecognitionResults(recognitions) {

            if (!(recognitions && recognitions.length > 0)) {
                result.innerHTML = "No recognitions returned";
                return;
            }

            // Sort descending
            recognitions.sort(function (a, b) {
                return b.confidence - a.confidence;
            });

            var html = "<table style='width:100%'><tr><th>#</th><th>Name</th><th>Confidence</th></tr>";
            for (var i = 0; i < recognitions.length; i++) {
                var recog = recognitions[i];
                html += "<tr><td>" + i + "</td><td>" + (recog.userid || "Face")
                    + "</td><td>" + confidence(recog.confidence) + "</td></tr>";
            }
            html += "</table>";
            result.innerHTML = html;
        }

        function displayMatchResults(match) {

            if (!match) {
                result.innerHTML = "No matches returned";
                return;
            }

            var html = "Match: " + confidence(match.similarity);
            result.innerHTML = html;
        }

        function displayBaseResults(base) {

            if (!base) {
                result.innerHTML = "No results returned";
                return;
            }

            var html = base.success ? "Operation successful" : "Operation failed";
            result.innerHTML = html;
        }

        function displayRegisteredFaces(data) {

            if (!data || data.faces == null || data.faces.length <= 0) {
                result.innerHTML = "No faces registered";
                return;
            }

            var html = "";
            for (var name of data.faces)
                html += name + "<br>";
            result.innerHTML = html;
        }

        function showSummaryText(text) {
            result.innerHTML = text;
        }

        function confidence(value) {
            if (value == undefined) return '';

            return Math.round(value * 100.0) + "%";
        }

        async function ping() {

            let url = serviceUrl.value.trim() + '/v1/status/ping';

            const controller = new AbortController()
            const timeoutSecs = parseInt(serviceTimeoutSecTxt.value);
            const timeoutId = setTimeout(() => controller.abort(), timeoutSecs * 1000)

            await fetch(url, { method: "GET", signal: controller.signal })
                .then(response => {

                    clearTimeout(timeoutId);

                    if (!response.ok)
                        setError('Error contacting API server');
                    else
                        response.json().then(data => {
                            // if (serverOnline == data.success)
                            //	return;

                            serverOnline = data.success;
                            if (serverOnline)
                                setStatus('API server is online', "green");
                            else
                                setError('Unable to contact API server');
                        })
                            .catch(error => {
                                setError('Error processing server response: ' + error)
                            });
                })
                .catch(error => {
                    if (error.name === 'AbortError') {
                        setError("Timed out waiting for a response. Maybe increase the timeout value?");
                        serverOnline = false;
                    }
                    else
                        setError(/*error.message*/ "Unable to contact AI Server");
                });
        }

        async function getAnalysisStatus() {

            // In the future we will ask for "logs since log ID 'x'" so we have
            // a full history. For now, a simple "last 10".
            let url = serviceUrl.value.trim() + '/v1/status/analysis/list';

            const controller = new AbortController()
            const timeoutSecs = parseInt(serviceTimeoutSecTxt.value);
            const timeoutId = setTimeout(() => controller.abort(), timeoutSecs * 1000)

            await fetch(url, { method: "GET", signal: controller.signal })
                .then(response => {

                    clearTimeout(timeoutId);

                    if (response.ok) {
                        response.json().then(data => {

                            if (data && data.statuses) {

                                // disable all first
                                var cards = document.getElementsByClassName('card');
                                for (const card of cards)
                                    card.classList.replace('d-flex', 'd-none');

                                var navTabs = document.getElementsByClassName('nav-item');
                                for (const tab of navTabs)
                                    tab.classList.add('d-none');

                                for (let i = 0; i < data.statuses.length; i++) {

                                    let moduleId = data.statuses[i].moduleId.replace(" ", "-");
                                    let queue    = data.statuses[i].queue;
                                    let running  = data.statuses[i].status == 'Started';

                                    // enable those that need enabling. Note that some cards will be
                                    // shared among modules, so we need to ensure we're not disabling
                                    // a card that was just enabled by a different module
                                    var cards = document.getElementsByClassName('card ' + queue);
                                    for (const card of cards) {
                                        if (running) {
                                            card.classList.replace('d-none', 'd-flex');
                                            let parent = card.parentNode;
                                            if (parent && parent.attributes["aria-labelledby"]) {
                                                let tabListItemId = parent.attributes["aria-labelledby"].value + "-listitem";
                                                let tabItem = document.getElementById(tabListItemId);
                                                if (tabItem.classList)
                                                    tabItem.classList.remove('d-none');
                                                card.parentNode.parentNode.classList.remove('d-none');
                                            }
                                        }
                                    }
                                }
                            }
                        })
                    }
                })
        }

        async function submitRequest(route, apiName, images, parameters, doneFunc) {

            setStatus("Sending request to AI server", "blue");

            let formData = new FormData();

            // Check file selected or not
            if (images && images.length > 0) {
                for (let i = 0; i < images.length; i++) {
                    file = images[i];
                    formData.append('image' + (i + 1), file);
                }
            }

            if (parameters && parameters.length > 0) {
                for (let i = 0; i < parameters.length; i++) {
                    keypair = parameters[i];
                    formData.append(keypair[0], keypair[1]);
                }
            }

            let url = serviceUrl.value.trim() + '/v1/' + route + '/' + apiName;

            result.innerHTML = "";

            const controller = new AbortController()
            const timeoutSecs = parseInt(serviceTimeoutSecTxt.value);
            const timeoutId = setTimeout(() => controller.abort(), timeoutSecs * 1000)

            await fetch(url, {
                method: "POST",
                body: formData,
                signal: controller.signal
            })
                .then(response => {

                    clearTimeout(timeoutId);

                    if (!response.ok)
                        setError('Error contacting API server');
                    else {
                        response.json().then(data => {
                            if (data) {
                                if (data.success) {
                                    doneFunc(data)
                                    setStatus("Call to " + apiName + " complete", "green");
                                }
                                else {
                                    result.innerHTML = data.error ?? "";
                                    setStatus("Call to " + apiName + " failed", "red");
                                }
                            } else
                                setError('No data was returned');
                        })
                            .catch(error => {
                                setError("Unable to process server response: " + error);
                            })
                    }
                })
                .catch(error => {
                    if (error.name === 'AbortError') {
                        setError("Timed out waiting for a response. Maybe increase the timeout value?");
                        serverOnline = false;
                    }
                    else
                        showSummaryText('Unable to complete API call: ' + error)
                });
        }

        function onDetectScene(fileChooser) {

            clearCanvas();

            if (fileChooser.files.length == 0) {
                alert("No file was selected for scene detection");
                return;
            }

            setImage(fileChooser.files[0]);
            let images = [fileChooser.files[0]];

            submitRequest('vision', 'scene', images, null, function (data) {
                result.innerHTML = "<table style='width:100%'>" +
                    "<tr><th>Label</th><th>Confidence</th></tr><tr>" +
                    "<tr><td> " + data.label + "</td>" +
                    "<td> " + confidence(data.confidence) + "</td ></tr> " +
                    "<table>";
            });
        }

        function onDetectFaces(fileChooser) {

            clearCanvas();

            if (fileChooser.files.length == 0) {
                alert("No file was selected for face detection");
                return;
            }

            setImage(fileChooser.files[0]);
            let images = [fileChooser.files[0]];

            submitRequest('vision', 'face', images, null, function (data) {
                drawRectangles(data.predictions);
                displayPredictionResults(data.predictions)
            });
        }

        function setCustomModelLists() {
            setCustomModelList(customModel);
            setCustomModelList(benchmarkModel, true);
        }

        function setCustomModelList(selectList, add_extras) {

            submitRequest('vision', 'custom/list', null, null, function (data) {

                while (selectList.options.length)
                    selectList.remove(0);

                if (add_extras) {
                    selectList.options.add(new Option("Standard model", ""));
                    selectList.options.add(new Option("No inference: round-trip speed test only", "round-trip"));
                }

                for (let i = 0; i < data.models.length; i++) {

                    let title;
                    switch (data.models[i].toLowerCase()) {
                        // case 'ipcam-general': title = 'General IP Cam'; break;
                        case 'license-plate': title = 'License Plates v2'; break;
                        case 'openlogo': title = 'Open Logo'; break;
                        case 'actionnetv2': title = '(Human) ActionNet v2'; break;
                        default: title = data.models[i];
                    }

                    var model = new Option(title, data.models[i]);
                    selectList.options.add(model);
                }
            });
        }

        function onCustomDetect(model_name, fileChooser) {

            clearCanvas();

            if (fileChooser.files.length == 0) {
                alert("No file was selected for custom object detection");
                return;
            }

            setImage(fileChooser.files[0]);
            let images = [fileChooser.files[0]];

            submitRequest('vision', 'custom/' + model_name, images, null, function (data) {
                drawRectangles(data.predictions);
                displayPredictionResults(data.predictions)
            });
        }

        function onDetectThings(fileChooser) {

            clearCanvas();

            if (fileChooser.files.length == 0) {
                alert("No file was selected for vision detection");
                return;
            }

            setImage(fileChooser.files[0]);
            let images = [fileChooser.files[0]];

            submitRequest('vision', 'detection', images, null, function (data) {
                drawRectangles(data.predictions);
                displayPredictionResults(data.predictions)
            });
        }

        function onCompareFaces(face1Ctrl, face2Ctrl) {

            clearCanvas();

            if (face1Ctrl.files.length == 0 || face2Ctrl.files.length == 0) {
                alert("Please supply two faces for face comparison");
                return;
            }

            setImage(face1Ctrl.files[0]);
            let images = [face1Ctrl.files[0], face2Ctrl.files[0]];

            submitRequest('vision', 'face/match', images, null, function (data) {
                displayMatchResults(data);
            });
        }

        function onRegisterFaces(userId, faceCtrl) {

            clearCanvas();

            if (faceCtrl.files.length == 0) {
                alert("No files were selected for face registration");
                return;
            }

            if (!userId) {
                alert("No name supplied");
                return;
            }

            setImage(faceCtrl.files[0]);

            var images = [];
            for (var i = 0; i < faceCtrl.files.length; i++)
                images.push(faceCtrl.files[i]);

            submitRequest('vision', 'face/register', images, [["userid", userId]], function (data) {
                if (data)
                    displayBaseResults(data);
                else
                    result.innerText = "Unable to register faces";
            });
        }

        function onListRegisteredFaces() {

            clearCanvas();

            submitRequest('vision', 'face/list', null, null, function (data) {
                displayRegisteredFaces(data)
            });
        }

        function onDeleteFace(userId) {

            clearCanvas();

            submitRequest('vision', 'face/delete', null, [["userid", userId]], function (data) {
                result.innerText = "Face deleted";
            });
        }

        function onRecogniseFace(fileChooser, minimumConfidence) {

            clearCanvas();

            if (fileChooser.files.length == 0) {
                alert("No file was selected for face recognition");
                return;
            }

            setImage(fileChooser.files[0]);
            let images = [fileChooser.files[0]];

            var params = []
            var minConfidence = parseFloat(minimumConfidence);
            if (!isNaN(minConfidence))
                params = [["min_confidence", minConfidence]]

            submitRequest('vision', 'face/recognize', images, params, function (data) {
                drawRectangles(data.predictions);
                displayRecognitionResults(data.predictions)
            });
        }

        function onRemoveBackground(fileChooser, use_alphamatting) {

            clearCanvas();

            if (fileChooser.files.length == 0) {
                alert("No file was selected for background removal");
                return;
            }

            setImage(fileChooser.files[0]);
            let images = [fileChooser.files[0]];
            let params = [["use_alphamatting", use_alphamatting]]

            submitRequest('image', 'removebackground', images, params, function (data) {
                result.innerText = "Background Removal complete";
                img.src = "data:image/png;base64," + data.imageBase64;
                img.style.visibility = "visible";
            });
        }

        function onBlurBackground(fileChooser, strength) {

            clearCanvas();

            if (fileChooser.files.length == 0) {
                alert("No file was selected for background bluring");
                return;
            }

            setImage(fileChooser.files[0]);
            let images = [fileChooser.files[0]];
            let params = [["strength", strength]]

            submitRequest('image', 'portraitfilter', images, params, function (data) {
                result.innerText = "Portrait mode complete";
                img.src = "data:image/png;base64," + data.filtered_image;
                img.style.visibility = "visible";
            });
        }

        function onSuperResolution(fileChooser) {

            clearCanvas();

            if (fileChooser.files.length == 0) {
                alert("No file was selected for image enhancement");
                return;
            }

            setImage(fileChooser.files[0]);
            let images = [fileChooser.files[0]];
            let params = null;

            submitRequest('image', 'superresolution', images, params, function (data) {
                result.innerText = "Resolution Improvement complete";
                img.src = "data:image/png;base64," + data.imageBase64;
                img.style.visibility = "visible";
            });
        }

        function onSummarizeText(articleText, numberOfSentences) {

            clearCanvas();

            if (!articleText) {
                alert("No text was provided for text summary");
                return;
            }

            var numberOfSentences = !isNaN(numberOfSentences) ? parseInt(numberOfSentences) : 2;
            var params = [['text', articleText], ["num_sentences", numberOfSentences]]

            submitRequest('text', 'summarize', null, params, function (data) {
                showSummaryText(data.summary || "No summary  was returned");
            });
        }

        function onAnalyzeSentiment(articleText) {

            clearCanvas();

            if (!articleText) {
                alert("No text was provided for sentiment analysis");
                return;
            }

            var params = [['text', articleText]]

            submitRequest('text', 'sentiment', null, params, function (data) {
                if (!data.success) {
                    result.innerText = "Sentiment analysis failed";
                }
                else {
                    result.innerText = "Probability that text is positive is " +
                        + (data.positive_probability * 100).toFixed(1) + "%. "
                        + "Text sentiment is " + (data.is_positive ? "positive" : "negative");
                }
            });
        }

        const useRequestAnimationFrame = true;
        const framePerSecond = 5; // Only if useRequestAnimationFrame = false

        let videoProcessingActive = false;
        let videoTimerId;
        let lastFrameDrawTime, lastInferenceTime, framesPerSecond, processedPerSecond;
        let rect_left, rect_top, rect_width, rect_height;
        let processQueueLength = 0;
        function onStartVideo(e) {

            const video = document.getElementById('video');

            if (navigator.mediaDevices.getUserMedia) {
                navigator.mediaDevices.getUserMedia({ video: true })
                    .then(function (stream) {
                        video.srcObject = stream;
                        videoProcessingActive = true;
                        lastFrameDrawTime = performance.now();
                        if (useRequestAnimationFrame)
                            requestAnimationFrame(drawVideoDetection);
                        else
                            videoTimerId = setInterval(drawVideoDetection, 1000 / framePerSecond);
                    })
                    .catch(function (err0r) {
                        console.log("Unable to capture webcam stream");
                    });
            }

            return false;
        }

        function drawVideoDetection() {

            const video = document.getElementById('video');
            const canvas = document.getElementById('canvas');
            canvas.width = video.videoWidth;
            canvas.height = video.videoHeight;

            let ctx = canvas.getContext('2d');

            // Draw the current image, the previous detection rect, and the stats
            ctx.drawImage(video, 0, 0); //, canvas.width, canvas.height);

            ctx.strokeStyle = "red";
            ctx.strokeRect(rect_left, rect_top, rect_width, rect_height);

            if (framesPerSecond) {
                ctx.font = '20px sans-serif';
                ctx.strokeStyle = "black";
                ctx.fillStyle = "white";
                ctx.textAlign = 'right';
                ctx.textBaseline = 'top';
                if (processedPerSecond)
                    ctx.fillText(`${framesPerSecond.toFixed(1)} FPS, ${processedPerSecond.toFixed(1)} detects/sec`, canvas.width - 10, 10);
                else
                    ctx.fillText(`${framesPerSecond.toFixed(1)} FPS`, canvas.width - 10, 10);
            }

            let previousFrameDrawTime = lastFrameDrawTime;
            lastFrameDrawTime = performance.now();

            // Draw previous face detection and move on
            if (processQueueLength > 0) {

                if (useRequestAnimationFrame && videoProcessingActive)
                    requestAnimationFrame(drawVideoDetection);

                return;
            }

            if (previousFrameDrawTime && previousFrameDrawTime < lastFrameDrawTime)
                framesPerSecond = 1000.0 / (lastFrameDrawTime - previousFrameDrawTime);

            canvas.toBlob(function (blob) {

                if (blob) {

                    let formData = new FormData();
                    formData.append('image', blob, 'filename.png');

                    let url = serviceUrl.value.trim() + '/v1/vision/face';

                    processQueueLength++;

                    fetch(url, {
                        method: "POST",
                        body: formData
                    })
                        .then(response => {
                            processQueueLength--;
                            if (response.ok) response.json().then(data => {
                                if (videoProcessingActive && data && data.success && data.predictions) {

                                    let previousInferenceTime = lastInferenceTime;
                                    lastInferenceTime = performance.now();

                                    if (previousInferenceTime && previousInferenceTime < lastInferenceTime)
                                        processedPerSecond = 1000.0 / (lastInferenceTime - previousInferenceTime);

                                    for (var i = 0; i < data.predictions.length; i++) {

                                        let rect = data.predictions[i];
                                        rect_left = Math.min(rect.x_min, rect.x_max);
                                        rect_top = Math.min(rect.y_min, rect.y_max);
                                        rect_width = Math.abs(rect.x_min - rect.x_max);
                                        rect_height = Math.abs(rect.y_min - rect.y_max);

                                        ctx.strokeStyle = "red";
                                        ctx.strokeRect(rect_left, rect_top, rect_width, rect_height);
                                    }
                                }
                            })
                        });
                }

                if (useRequestAnimationFrame && videoProcessingActive)
                    requestAnimationFrame(drawVideoDetection);
            });
        }

        function onStopVideo(e) {

            videoProcessingActive = false;
            if (!useRequestAnimationFrame)
                clearInterval(videoTimerId);

            var stream = video.srcObject;
            try {
                var tracks = stream.getTracks();

                for (var i = 0; i < tracks.length; i++) {
                    var track = tracks[i];
                    track.stop();
                }
            }
            catch { }

            video.srcObject = null;

            return false;
        }

        let isBenchmarkCancelled;
        let benchmarkQueueLength = 0;
        const maxQueueLength = 9;

        function onBenchmark(fileChooser, model_name, resultElm) {

            clearCanvas();

            let images = null;
            let route = model_name ? ('custom/' + model_name) : 'detection';

            if (benchmarkModel.value == "round-trip") {
                route = "custom/list";
            }
            else {
                if (fileChooser.files.length == 0) {
                    alert("No file was selected for benchmarking");
                    return;
                }

                setImage(fileChooser.files[0]);
                images = [fileChooser.files[0]];
            }

            let startMilliseconds = performance.now();
            let nResponses = 0;

            // Warm up
            benchmarkQueueLength = 0;
            for (let i = 0; i < 3; i++) {
                benchmarkQueueLength++;
                submitRequest('vision', route, images, null, function (data) {
                    benchmarkQueueLength--;
                    resultElm.innerHTML = "Warm up " + i;
                });
            }

            // Benchmark
            for (let i = 0; i < 50; i++) {

                waitForQueueToSubside(benchmarkQueueLength);

                benchmarkQueueLength++;
                submitRequest('vision', route, images, null, function (data) {
                    benchmarkQueueLength--;
                    let currentMilliseconds = performance.now();
                    nResponses++;
                    let opsPerSecond = (nResponses * 1000) / (currentMilliseconds - startMilliseconds);
                    resultElm.innerHTML = opsPerSecond.toFixed(1) + " operations per second - " + nResponses + "/50";
                });
            }
        }

        function waitForQueueToSubside() {

            if (benchmarkQueueLength < maxQueueLength)
                return;

            console.log(`Queue is ${benchmarkQueueLength}. Waiting until it's below ${maxQueueLength}`);

            // try again in 10 milliseconds
            setTimeout(waitForQueueToSubside, 10);
        }

    </script>

</head>

<body class="dark-mode">
    <div>
        <div class=" mx-auto p-3" style="max-width:1100px;">

            <div class="w-100 header d-flex justify-content-between">
                <a href="https://codeproject.github.io/codeproject.ai" style="margin:2px 0 2px 5px">
                    <svg version="1.1" xmlns="http://www.w3.org/2000/svg"
                         xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24" role="img"
                         style="height: 60px; margin-right: -12px;">
                        <title>CodeProject Icon</title>
                        <path id="svg_2" d="M9.052,21.798c-0.236,0-0.459-0.905-0.463-1.145c-0.007-0.356,0.038-0.755,0.473-0.755
		c0.436,0,0.47,0.398,0.473,0.755C9.542,20.982,9.288,21.798,9.052,21.798z M10.903,23.985c0.729,0.004,3.677,0.151,3.715-0.847
		c0.041-0.998-1.401-1.525-2.234-1.563c-0.837-0.037-2.143,0.467-2.431,0.209c-0.377-0.339,0.281-1.882,0.531-2.188
		c0.172-0.205,0.453-0.524,0.606-0.664c0.594-0.553,0.967-1.32,0.964-2.178c-0.004-0.939-0.281-1.278,0.658-0.531
		c0.325,0.257,0.768,0.59,1.336,0.854c1.139,0.524-1.319-1.68-1.789-2.102c-0.788-0.699-1.045-1.08-0.609-1.354
		c0.346-0.216,0.672-0.459,0.977-0.723c1.196-1.035,2.046-2.605,1.878-4.896C14.354,5.911,10.416,0.024,9.364,0
		c-0.85-0.017-2.578,2.108-3.527,3.257C4.809,4.505,3.201,6.665,2.879,7.934C2.783,8.311,2.746,8.708,2.746,9.113
		c0,1.44,0.75,2.784,1.898,3.744c0.703,0.586,1.481,1.039,0.237,2.311c-0.74,0.758-2.434,2.361-1.453,1.909
		c0.572-0.264,1-0.614,1.34-0.854c0.709-0.504,0.99-0.669,0.833-0.031c-0.034,0.134-0.028,0.274-0.028,0.411
		c0,0.892,0.37,1.69,0.95,2.277c0.151,0.154,0.586,0.486,0.833,0.699c0.586,0.507,1.196,1.865,0.816,2.204
		c-0.288,0.258-1.594-0.246-2.427-0.209c-0.836,0.038-2.275,0.565-2.238,1.563s3.17,0.851,3.9,0.857
		c1.645,0.013,1.09-0.642,1.645-0.631C9.607,23.379,9.052,23.976,10.903,23.985z" />
                        <path id="svg_3" fill="#C3DE6C" d="M9.802,1.187c-0.668-0.017-1.686,1.878-2.43,2.863C6.567,5.115,5.354,7.004,5.1,8.091
		C5.025,8.414,4.997,8.753,4.997,9.099c0,2.51,2.481,4.608,4.792,4.608c1.155,0,2.132-0.604,2.938-1.368
		c0.935-0.888,1.597-2.232,1.47-4.193C14.076,6.36,10.622,1.207,9.802,1.187z" />
                        <path id="svg_4" d="M10.649,10.378c0.336,0.456,0.716,0.775,1.172,0.775c0.919,0,1.662-1.187,1.662-2.646
		c0-1.464-0.743-2.65-1.662-2.65c-0.456,0-0.85,0.309-1.172,0.775C10.491,6.86,10.2,7.042,9.823,6.401
		c-0.35-0.593-0.918-0.922-1.508-0.922c-1.179,0-2.135,1.412-2.135,3.154s0.956,3.154,2.135,3.154c0.737,0,1.371-0.556,1.769-1.389
		C10.238,10.073,10.47,10.135,10.649,10.378z" />
                        <path id="svg_5" fill="#FFFFFF" d="M9.812,8.633c0,1.221-0.671,2.211-1.497,2.211c-0.826,0-1.498-0.991-1.498-2.211
		c0-1.224,0.672-2.215,1.498-2.215C9.141,6.418,9.812,7.409,9.812,8.633z" />
                        <path id="svg_6" fill="#FFFFFF" d="M12.987,8.506c0,1.025-0.521,1.854-1.166,1.854c-0.641,0-1.162-0.83-1.162-1.854
		c0-1.029,0.521-1.858,1.162-1.858C12.465,6.648,12.987,7.478,12.987,8.506z" />
                        <path id="svg_7" fill="#C3DE6C" d="M11.564,16.813c0,1.217-0.99,2.201-2.218,2.201c-1.227,0-2.221-0.984-2.221-2.201
		s0.994-2.205,2.221-2.205C10.574,14.608,11.564,15.597,11.564,16.813z" />
                        <path id="svg_8" d="M9.877,17.434c0,0.229-0.188,0.415-0.418,0.415s-0.418-0.186-0.418-0.415S9.23,17.02,9.459,17.02
		S9.877,17.204,9.877,17.434z" />
                        <path id="svg_9" d="M2.311,17.358c-0.165,0-1.687,0.021-2.163,0.227c-0.473,0.209,0.285,0.507,0.929,0.545
		c0.322,0.021,0.219,0.257,0.075,0.452c-1.155,1.581-0.264,1.262,0.514,0.565c0.192-0.171,0.264,0.056,0.284,0.265
		c0.018,0.206,0.511,1.749,0.665-0.209c0.062-0.81,0.113-1.241,0.076-1.525C2.653,17.396,2.499,17.358,2.311,17.358z" />
                        <path id="svg_10" fill="#C3DE6C" d="M8.037,22.837c-0.284-0.527-1.063-0.452-1.422-0.395c-0.36,0.056-1.724,0.374-1.498,0.771
		c0.229,0.395,1.1,0.433,1.765,0.151C7.543,23.08,8.37,23.451,8.037,22.837z" />
                        <path id="svg_11" d="M15.169,17.358c0.161,0,1.686,0.021,2.163,0.227c0.473,0.209-0.285,0.507-0.933,0.545
		c-0.319,0.021-0.216,0.257-0.075,0.452c1.158,1.581,0.267,1.262-0.511,0.565c-0.192-0.171-0.264,0.056-0.284,0.265
		c-0.021,0.206-0.515,1.749-0.665-0.209c-0.063-0.81-0.113-1.241-0.076-1.525C14.827,17.396,14.977,17.358,15.169,17.358z" />
                        <path id="svg_12" fill="#C3DE6C" d="M10.086,22.837c0.284-0.527,1.063-0.452,1.423-0.395c0.359,0.056,1.728,0.374,1.497,0.771
		c-0.226,0.395-1.1,0.433-1.765,0.151C10.581,23.08,9.754,23.451,10.086,22.837z" />
                        <path id="svg_13" d="M8.915,8.547c0,0.607-0.236,1.101-0.524,1.101c-0.288,0-0.521-0.494-0.521-1.101S8.103,7.447,8.39,7.447
		C8.678,7.447,8.915,7.94,8.915,8.547z" />
                        <path id="svg_14" d="M12.291,8.455c0,0.483-0.188,0.877-0.418,0.877s-0.418-0.394-0.418-0.877c0-0.487,0.188-0.881,0.418-0.881
		S12.291,7.968,12.291,8.455z" />
                    </svg><span class="site-title"><span class="front">Code</span><span class="end">Project</span></span>
                </a>

                <!--<div style="font-size: 2.1rem; margin-top: 12px;">CodeProject.AI Explorer</div>-->

                <a href="https://codeproject.github.io/codeproject.ai" style="margin:0px 5px 0 0">
                    <svg viewBox="0 0 660 160" xmlns="http://www.w3.org/2000/svg" style="height:60px">
                        <defs>
                            <style>
                                .cls-1 {
                                    fill: #fff;
                                }

                                .cls-2 {
                                    fill: #b3ddca;
                                }
                            </style>
                        </defs>
                        <g>
                            <title>CodeProject.AI</title>
                            <path id="svg_1" class="cls-1" d="m549.22,119.94l28.57,-87.75l14.15,0l28.57,87.75l-11,0l-8.81,-26.27l-31.71,0l-8.82,26.27l-10.95,0zm22.95,-36l25.4,0l-12.7,-38l-12.7,38z" fill="#231f20" />
                            <path id="svg_2" class="cls-1" d="m619.73,119.94l0,-87.72l11,0l0,87.72l-11,0z" fill="#231f20" />
                            <path id="svg_3" class="cls-1" d="M 56.1 58.8 Q 57.3 59.6 57.5 60.9 Q 57.7 62.1 56.9 63.3 Q 56.2 64.2 55.1 64.3 Q 54 64.4 53 63.9 Q 49.3 61.4 45 60 Q 40.7 58.7 35.7 58.7 Q 29.7 58.7 24.5 60.8 Q 19.3 62.9 15.4 66.9 Q 11.5 70.9 9.4 76.4 Q 7.3 81.8 7.3 88.4 Q 7.3 95.1 9.5 100.6 Q 11.6 106.1 15.6 110 Q 19.5 114 24.6 116.1 Q 29.8 118.1 35.7 118.1 Q 40.6 118.1 44.9 116.8 Q 49.2 115.4 52.9 113 Q 53.9 112.4 55.1 112.6 Q 56.2 112.7 56.9 113.7 Q 57.8 114.8 57.5 116 Q 57.3 117.3 56.2 118 Q 53.7 119.5 50.4 120.9 Q 47 122.3 43.2 123.1 Q 39.5 124 35.7 124 Q 28.6 124 22.3 121.4 Q 16.1 118.8 11.2 114.1 Q 6.4 109.4 3.7 102.9 Q 1 96.4 1 88.4 Q 1 80.6 3.6 74.1 Q 6.3 67.7 11.1 62.9 Q 15.8 58.2 22.1 55.5 Q 28.5 52.8 35.7 52.8 Q 41.3 52.8 46.6 54.4 Q 51.8 56 56.1 58.8 Z M 114.3 97.7 Q 114.3 105.2 111 111.2 Q 107.8 117.2 102.1 120.6 Q 96.5 124 89.2 124 Q 82.2 124 76.5 120.6 Q 70.8 117.2 67.5 111.2 Q 64.2 105.2 64.2 97.7 Q 64.2 90 67.5 84.1 Q 70.8 78.2 76.5 74.7 Q 82.2 71.2 89.2 71.2 Q 96.5 71.2 102.1 74.7 Q 107.8 78.2 111 84.1 Q 114.3 90 114.3 97.7 Z M 108.4 97.7 Q 108.4 91.6 105.9 86.9 Q 103.4 82.2 99.1 79.5 Q 94.8 76.8 89.2 76.8 Q 83.9 76.8 79.5 79.5 Q 75.2 82.2 72.7 86.9 Q 70.1 91.6 70.1 97.7 Q 70.1 103.7 72.7 108.3 Q 75.2 113 79.5 115.7 Q 83.9 118.4 89.2 118.4 Q 94.8 118.4 99.1 115.7 Q 103.4 113 105.9 108.3 Q 108.4 103.7 108.4 97.7 Z M 166.9 49.7 Q 168.2 49.7 169.1 50.6 Q 169.9 51.4 169.9 52.7 L 169.9 120 Q 169.9 121.2 169 122.1 Q 168.1 123 166.9 123 Q 165.7 123 164.8 122.1 Q 164 121.2 164 120 L 164 108.2 L 165.7 106.9 Q 165.7 109.6 164.2 112.7 Q 162.7 115.7 160 118.2 Q 157.2 120.8 153.6 122.4 Q 150 124 145.9 124 Q 139.2 124 133.9 120.5 Q 128.6 117.1 125.6 111.1 Q 122.5 105.2 122.5 97.6 Q 122.5 90 125.6 84 Q 128.6 78.1 133.9 74.6 Q 139.1 71.2 145.8 71.2 Q 149.8 71.2 153.5 72.7 Q 157.1 74.2 160 76.8 Q 162.8 79.3 164.4 82.7 Q 166 86 166 89.5 L 164 88.1 L 164 52.7 Q 164 51.5 164.8 50.6 Q 165.7 49.7 166.9 49.7 Z M 146.3 118.4 Q 151.7 118.4 155.7 115.7 Q 159.7 113 162 108.3 Q 164.4 103.6 164.4 97.6 Q 164.4 91.6 162 86.9 Q 159.7 82.2 155.7 79.5 Q 151.6 76.8 146.3 76.8 Q 141.2 76.8 137.1 79.5 Q 133 82.2 130.7 86.9 Q 128.3 91.6 128.3 97.6 Q 128.3 103.5 130.7 108.2 Q 133 113 137.1 115.7 Q 141.2 118.4 146.3 118.4 Z M 206.8 124 Q 199.2 124 193.4 120.7 Q 187.5 117.5 184.3 111.6 Q 181 105.8 181 98 Q 181 89.5 184.3 83.6 Q 187.6 77.7 192.9 74.4 Q 198.2 71.2 204.2 71.2 Q 208.5 71.2 212.6 72.8 Q 216.7 74.3 219.9 77.3 Q 223.2 80.3 225.1 84.7 Q 227 89 227.2 94.8 Q 227.1 96 226.2 96.8 Q 225.3 97.7 224.1 97.7 L 184.5 97.7 L 183.3 92.3 L 222.3 92.3 L 221 93.5 L 221 91.5 Q 220.6 86.9 218 83.6 Q 215.4 80.3 211.8 78.6 Q 208.1 77 204.2 77 Q 201.2 77 198.1 78.2 Q 194.9 79.3 192.4 81.9 Q 189.9 84.4 188.3 88.3 Q 186.7 92.2 186.7 97.6 Q 186.7 103.4 189.1 108.2 Q 191.5 112.9 196 115.7 Q 200.5 118.4 206.7 118.4 Q 210.2 118.4 213 117.5 Q 215.7 116.5 217.8 114.8 Q 219.9 113.2 221.3 111.4 Q 222.4 110.5 223.5 110.5 Q 224.4 110.5 225.2 111.3 Q 225.9 112.1 225.9 113.1 Q 225.9 114.3 224.9 115.2 Q 222 118.7 217.2 121.4 Q 212.5 124 206.8 124 Z M 266.3 53.7 Q 271.9 53.7 276.1 56.2 Q 280.4 58.7 282.8 63.2 Q 285.3 67.7 285.3 73.4 Q 285.3 79.1 282.8 83.7 Q 280.4 88.4 276.1 91 Q 271.9 93.7 266.3 93.7 L 245.3 93.7 L 245.8 92.7 L 245.8 120 Q 245.8 121.2 244.9 122.1 Q 244 123 242.8 123 Q 241.5 123 240.7 122.1 Q 239.9 121.2 239.9 120 L 239.9 56.7 Q 239.9 55.5 240.8 54.6 Q 241.7 53.7 242.8 53.7 Z M 266.3 88 Q 270.2 88 273 86 Q 275.9 84.1 277.5 80.8 Q 279.2 77.5 279.2 73.4 Q 279.2 69.3 277.5 66.2 Q 275.9 63.1 273 61.3 Q 270.2 59.5 266.3 59.5 L 245.3 59.5 L 245.8 58.7 L 245.8 88.5 L 245.2 88 Z M 298.9 123 Q 297.5 123 296.7 122.1 Q 295.9 121.2 295.9 120 L 295.9 75.7 Q 295.9 74.5 296.7 73.6 Q 297.6 72.7 298.9 72.7 Q 300.1 72.7 301 73.6 Q 301.8 74.5 301.8 75.7 L 301.8 90.5 L 300.3 90.8 Q 300.6 87.4 302.1 83.9 Q 303.5 80.5 306 77.7 Q 308.5 74.8 311.9 73 Q 315.3 71.2 319.6 71.2 Q 321.3 71.2 323 72 Q 324.7 72.7 324.7 74.4 Q 324.7 75.9 323.9 76.7 Q 323.1 77.5 322 77.5 Q 321.1 77.5 320.1 77 Q 319.1 76.5 317.4 76.5 Q 314.6 76.5 311.8 78.1 Q 309.1 79.7 306.8 82.5 Q 304.5 85.3 303.2 88.7 Q 301.8 92.1 301.8 95.6 L 301.8 120 Q 301.8 121.2 300.9 122.1 Q 300 123 298.9 123 Z M 377.5 97.7 Q 377.5 105.2 374.2 111.2 Q 370.9 117.2 365.3 120.6 Q 359.6 124 352.4 124 Q 345.4 124 339.7 120.6 Q 334 117.2 330.7 111.2 Q 327.4 105.2 327.4 97.7 Q 327.4 90 330.7 84.1 Q 334 78.2 339.7 74.7 Q 345.4 71.2 352.4 71.2 Q 359.6 71.2 365.3 74.7 Q 370.9 78.2 374.2 84.1 Q 377.5 90 377.5 97.7 Z M 371.5 97.7 Q 371.5 91.6 369 86.9 Q 366.6 82.2 362.3 79.5 Q 357.9 76.8 352.4 76.8 Q 347.1 76.8 342.7 79.5 Q 338.3 82.2 335.8 86.9 Q 333.3 91.6 333.3 97.7 Q 333.3 103.7 335.8 108.3 Q 338.3 113 342.7 115.7 Q 347.1 118.4 352.4 118.4 Q 357.9 118.4 362.3 115.7 Q 366.6 113 369 108.3 Q 371.5 103.7 371.5 97.7 Z M 395.9 127.6 Q 395.9 132.3 393.6 135.6 Q 391.4 138.9 388.1 140.7 Q 384.9 142.5 381.6 142.5 Q 380.3 142.5 379.6 141.7 Q 378.8 140.9 378.8 139.8 L 378.8 139.4 Q 378.8 138.2 379.6 137.6 Q 380.4 136.9 381.4 136.7 Q 383.7 136.2 385.6 135 Q 387.5 133.8 388.7 131.7 Q 389.9 129.6 389.9 126.7 L 389.9 75.6 Q 389.9 74.4 390.8 73.5 Q 391.6 72.6 392.9 72.6 Q 394.2 72.6 395 73.5 Q 395.9 74.4 395.9 75.6 Z M 392.9 64.8 Q 390.8 64.8 389.8 63.8 Q 388.7 62.9 388.7 61.1 L 388.7 60.1 Q 388.7 58.4 389.9 57.4 Q 391 56.5 393 56.5 Q 394.9 56.5 395.9 57.4 Q 396.9 58.4 396.9 60.1 L 396.9 61.1 Q 396.9 62.9 395.9 63.8 Q 394.9 64.8 392.9 64.8 Z M 436.7 124 Q 429.1 124 423.3 120.7 Q 417.4 117.5 414.1 111.6 Q 410.9 105.8 410.9 98 Q 410.9 89.5 414.2 83.6 Q 417.5 77.7 422.8 74.4 Q 428.1 71.2 434 71.2 Q 438.4 71.2 442.5 72.8 Q 446.6 74.3 449.8 77.3 Q 453.1 80.3 455 84.7 Q 456.9 89 457.1 94.8 Q 457 96 456.1 96.8 Q 455.2 97.7 454 97.7 L 414.3 97.7 L 413.2 92.3 L 452.2 92.3 L 450.9 93.5 L 450.9 91.5 Q 450.5 86.9 447.9 83.6 Q 445.3 80.3 441.7 78.6 Q 438 77 434 77 Q 431.1 77 428 78.2 Q 424.8 79.3 422.3 81.9 Q 419.8 84.4 418.2 88.3 Q 416.6 92.2 416.6 97.6 Q 416.6 103.4 419 108.2 Q 421.4 112.9 425.9 115.7 Q 430.4 118.4 436.6 118.4 Q 440.1 118.4 442.9 117.5 Q 445.6 116.5 447.7 114.8 Q 449.8 113.2 451.2 111.4 Q 452.3 110.5 453.4 110.5 Q 454.3 110.5 455.1 111.3 Q 455.8 112.1 455.8 113.1 Q 455.8 114.3 454.8 115.2 Q 451.9 118.7 447.1 121.4 Q 442.4 124 436.7 124 Z M 490.6 71.2 Q 495.3 71.2 499.3 72.3 Q 503.2 73.3 505.7 75.1 Q 508.1 76.9 508.1 78.9 Q 508.1 79.9 507.4 80.9 Q 506.7 81.8 505.5 81.8 Q 504.3 81.8 503.6 81.2 Q 502.8 80.6 502.1 79.8 Q 501.4 79 500 78.4 Q 498.7 77.8 496.3 77.3 Q 493.9 76.8 490.9 76.8 Q 485.3 76.8 480.9 79.5 Q 476.4 82.3 473.8 87 Q 471.3 91.7 471.3 97.6 Q 471.3 103.5 473.7 108.2 Q 476.2 113 480.6 115.7 Q 484.9 118.4 490.8 118.4 Q 494.7 118.4 496.9 117.8 Q 499.1 117.1 500.7 116.2 Q 502.5 115.1 503.4 114 Q 504.2 113 505.6 113 Q 506.9 113 507.5 113.7 Q 508.2 114.5 508.2 115.7 Q 508.2 117.3 505.9 119.2 Q 503.5 121.1 499.5 122.6 Q 495.4 124 490.2 124 Q 482.7 124 477.1 120.5 Q 471.6 117.1 468.4 111.1 Q 465.3 105.1 465.3 97.6 Q 465.3 90.3 468.5 84.4 Q 471.7 78.5 477.3 74.8 Q 483 71.2 490.6 71.2 Z M 514.8 72.6 L 537.9 72.6 Q 539.1 72.6 539.9 73.5 Q 540.6 74.3 540.6 75.5 Q 540.6 76.6 539.9 77.4 Q 539.1 78.2 537.9 78.2 L 514.8 78.2 Q 513.6 78.2 512.8 77.3 Q 511.9 76.5 511.9 75.4 Q 511.9 74.2 512.8 73.4 Q 513.6 72.6 514.8 72.6 Z M 525 58.7 Q 526.3 58.7 527.1 59.5 Q 528 60.4 528 61.6 L 528 111.3 Q 528 114.1 528.7 115.5 Q 529.5 116.9 530.6 117.3 Q 531.8 117.8 533 117.8 Q 533.8 117.8 534.5 117.5 Q 535.1 117.3 535.9 117.3 Q 536.8 117.3 537.4 118 Q 538.1 118.6 538.1 119.7 Q 538.1 121.1 536.5 122.1 Q 534.9 123 532.7 123 Q 531.8 123 530 122.9 Q 528.3 122.7 526.4 121.8 Q 524.5 120.8 523.3 118.4 Q 522 116.1 522 111.6 L 522 61.6 Q 522 60.4 522.9 59.5 Q 523.7 58.7 525 58.7 Z"></path>

                            <rect x="554.7" width="22.1" height="11.6" style="stroke-opacity: 0.33; fill: rgb(179, 221, 202); fill-opacity: 0.33;" />
                            <rect x="344.3" width="22.1" height="11.6" style="stroke-opacity: 0.33; fill: rgb(179, 221, 202); fill-opacity: 0.33;" y="32.4" transform="matrix(0, -1, 1, 0, 610.649994, 397.350006)" />
                            <rect x="601.6" width="22.1" height="11.6" style="stroke-opacity: 0.33; fill: rgb(179, 221, 202); fill-opacity: 0.33;" y="146.3" />
                            <rect x="344.3" width="22.1" height="11.6" style="stroke-opacity: 0.33; fill: rgb(179, 221, 202); fill-opacity: 0.33;" y="32.4" transform="matrix(0, -1, 1, 0, 491.309998, 471.269989)" />

                            <polygon id="svg_4" class="cls-2" points="535.239990234375,55.010009765625 523.7100219726562,55.010009765625 523.7100219726562,1 554.6600341796875,1 554.6600341796875,12.529998779296875 535.239990234375,12.529998779296875 535.239990234375,55.010009765625 " fill="#b3ddca" />
                            <polygon id="svg_5" class="cls-2" points="577.719970703125,158.91998291015625 523.7100219726562,158.91998291015625 523.7100219726562,127.97000122070312 535.239990234375,127.97000122070312 535.239990234375,147.3900146484375 577.719970703125,147.3900146484375 577.719970703125,158.91998291015625 " fill="#b3ddca" />
                            <polygon id="svg_6" class="cls-2" points="654.6500244140625,158.91998291015625 623.7000122070312,158.91998291015625 623.7000122070312,147.3900146484375 643.1199951171875,147.3900146484375 643.1199951171875,104.91000366210938 654.6500244140625,104.91000366210938 654.6500244140625,158.91998291015625 " fill="#b3ddca" />
                            <polygon id="svg_7" class="cls-2" points="654.6500244140625,31.95001220703125 643.1199951171875,31.95001220703125 643.1199951171875,12.529998779296875 600.6400146484375,12.529998779296875 600.6400146484375,1 654.6500244140625,1 654.6500244140625,31.95001220703125 " fill="#b3ddca" />
                        </g>
                    </svg>
                </a>
            </div>

            <div class="card mt-3">
                <div class="card-body">

                    <div class="form-group row text-nowrap">
                        <label class="col-form-label col-2 text-end">Service API URL</label>
                        <input class="col-form-control col-2" type="text" id="serviceUrl" style="width:12.5rem" />
                        <label class="col-form-label col-1 text-end">Timeout</label>
                        <input class="col-form-control col-1" type="number" id="serviceTimeoutSecTxt" style="width:4.5rem" />
                        <label class="col-form-label col-1" style="width:3rem">sec</label>
                        <label id="status" class="col-form-label col-5 bg-white fw-bold ms-4"></label>
                    </div>
                </div>
            </div>

            <form method="post" action="" enctype="multipart/form-data" id="myform">
                <div class="d-flex align-content-between mt-4">

                    <div style="width:600px">

                        <ul class="nav nav-tabs d-flex" id="DemoTabs" role="tablist">

                            <li class="nav-item d-none" id="vision-tab-listitem">
                                <button class="nav-link active" id="vision-tab" data-bs-toggle="tab" data-bs-target="#vision-panel" type="button" role="tab" aria-controls="vision" aria-selected="true">Vision</button>
                            </li>
                            <li class="nav-item d-none" id="image-tab-listitem">
                                <button class="nav-link" id="image-tab" data-bs-toggle="tab" data-bs-target="#image-panel" type="button" role="tab" aria-controls="image" aria-selected="true">Image</button>
                            </li>

                            <li class="nav-item d-none" id="face-tab-listitem">
                                <button class="nav-link" id="face-tab" data-bs-toggle="tab" data-bs-target="#face-panel" type="button" role="tab" aria-controls="face" aria-selected="false">Face</button>
                            </li>
                            <li class="nav-item d-none" id="text-tab-listitem">
                                <button class="nav-link" id="text-tab" data-bs-toggle="tab" data-bs-target="#text-panel" type="button" role="tab" aria-controls="text" aria-selected="false">Text</button>
                            </li>

                            <li class="nav-item d-none" id="video-tab-listitem">
                                <button class="nav-link" id="text-tab" data-bs-toggle="tab" data-bs-target="#video-panel" type="button" role="tab" aria-controls="video" aria-selected="false">Video</button>
                            </li>

                            <li class="nav-item ms-auto" id="benchmark-tab-listitem">
                                <button class="nav-link" id="benchmark-tab" data-bs-toggle="tab" data-bs-target="#benchmark-panel" type="button" role="tab" aria-controls="benchmark" aria-selected="false">Benchmark</button>
                            </li>
                        </ul>

                        <div class="tab-content" id="DemoTabsContent">

                            <div class="tab-pane fade show active" id="vision-panel" role="tabpanel" aria-labelledby="vision-tab">

                                <div class="card mt-3 d-none objectdetection_queue">
                                    <div class="card-header h3">Object Detection</div>
                                    <div class="card-body">

                                        <div class="form-group row">
                                            <label class="col-form-label col-3">Image</label>
                                            <input class="col form-control btn-light" id="image" type="file" onchange="return previewImage(this)" />
                                        </div>
                                        <div class="form-group row mt-2 flex-nowrap">
                                            <label class="col-form-label col-3">Operation</label>
                                            <input class="col form-control btn-success" type="button" value="Detect Scene" id="scene" onclick="onDetectScene(image)" />
                                            <input class="col form-control btn-info" type="button" value="Detect Face" id="face" onclick="onDetectFaces(image)" />
                                            <input class="col form-control btn-warning" type="button" value="Detect Things" id="things" onclick="onDetectThings(image)" />
                                        </div>
                                    </div>
                                </div>

                                <div class="card mt-3 d-none objectdetection_queue">
                                    <div class="card-header h3">Custom Object Detection</div>
                                    <div class="card-body">

                                        <div class="form-group row">
                                            <label class="col-form-label col-3">Image</label>
                                            <input class="col form-control btn-light" id="customimage" type="file"
                                                   onchange="return previewImage(this)" />
                                        </div>
                                        <div class="form-group d-flex justify-content-end mt-2">
                                            <select id="customModel" class="form-select me-5">
                                                <option value="ipcam-general">General IP Cam</option>
                                                <option value="license-plate">License Plates v2</option>
                                                <option value="openlogo">Open Logo</option>
                                                <option value="actionnetv2">(Human) ActionNet v2</option>
                                            </select>
                                            <input class="btn btn-warning" type="button"
                                                   value="Run Detection" id="customthings"
                                                   onclick="onCustomDetect(customModel.value, customimage)" />
                                        </div>
                                    </div>
                                </div>
                            </div>

                            <div class="tab-pane fade show" id="image-panel" role="tabpanel" aria-labelledby="image-tab">

                                <div class="card mt-3 d-none backgroundremover_queue">
                                    <div class="card-header h3">Background Removal</div>
                                    <div class="card-body">

                                        <div class="form-group row">
                                            <label class="col-form-label col-2">Image</label>
                                            <input class="col form-control btn-light" id="removeBgInput" type="file" style="width:17rem"
                                                   onchange="return previewImage(this)" />
                                            <input class="form-control btn-success" type="button" value="Remove Background"
                                                   style="width:11rem" id="recognise"
                                                   onclick="onRemoveBackground(removeBgInput, use_alphamatting.checked)" />
                                        </div>

                                        <div class="form-group row mt-1 justify-content-end">
                                            <div class="checkbox checkbox-primary col-4">
                                                <input id="use_alphamatting" class="styled" type="checkbox" checked>
                                                <label for="use_alphamatting">Use Alpha matting</label>
                                            </div>
                                        </div>
                                    </div>
                                </div>

                                <div class="card mt-3 d-none portraitfilter_queue">
                                    <div class="card-header h3">Background blurring</div>
                                    <div class="card-body">

                                        <div class="form-group row">
                                            <label class="col-form-label col-2">Image</label>
                                            <input class="col form-control btn-light" id="blurBgInput" type="file" style="width:17rem"
                                                   onchange="return previewImage(this)" />
                                            <input class="form-control btn-success" type="button" value="Blur Background" style="width:10rem"
                                                   id="recognise" onclick="onBlurBackground(blurBgInput, strength.value)" />
                                        </div>

                                        <div class="form-group row mt-1 justify-content-end">
                                            <label class="col-3 col-form-label text-end">Blur Strength</label>
                                            <input class="form-control" id="strength" style="width:15rem"
                                                   type="range" min="0.0" max="1.0" step="0.1" value="1.0"
                                                   oninput="document.getElementById('strengthLabel').innerHTML=this.value;">
                                            <span id="strengthLabel" class="col-form-label text-nowrap col-1">1.0</span>
                                        </div>
                                    </div>
                                </div>

                                <div class="card mt-3 d-none superresolution_queue">
                                    <div class="card-header h3">Increase Resolution</div>
                                    <div class="card-body">

                                        <div class="form-group row">
                                            <label class="col-form-label col-2">Image</label>
                                            <input class="col form-control btn-light" id="superResInput" type="file" style="width:17rem"
                                                   onchange="return previewImage(this)" />
                                            <input class="form-control btn-success" type="button" value="Super Resolution"
                                                   style="width:11rem" id="superresolution"
                                                   onclick="onSuperResolution(superResInput)" />
                                        </div>
                                    </div>
                                </div>

                            </div>

                            <div class="tab-pane fade show" id="face-panel" role="tabpanel" aria-labelledby="face-tab">

                                <div class="card mt-3 d-none faceprocessing_queue">
                                    <div class="card-header h3">Face Comparison</div>
                                    <div class="card-body">
                                        <div class="d-flex align-items-center">
                                            <div class="w-75">
                                                <div class="form-group row">
                                                    <label class="col-form-label col-3">Image 1</label>
                                                    <input class="col form-control btn-light" id="face1" type="file" onchange="return previewImage(this)" />
                                                </div>
                                                <div class="form-group row">
                                                    <label class="col-form-label col-3">Image 2</label>
                                                    <input class="col form-control btn-light" id="face2" type="file" onchange="return previewImage(this)" />
                                                </div>
                                            </div>
                                            <div style="width:10rem" class="text-end">
                                                <input type="button" value="Compare" class="btn btn-success" onclick="onCompareFaces(face1, face2)" />
                                            </div>
                                        </div>
                                    </div>
                                </div>

                                <div class="card mt-3 d-none faceprocessing_queue">
                                    <div class="card-header h3">Face Registration</div>
                                    <div class="card-body">

                                        <div>
                                            <div class="form-group row">
                                                <label class="col-form-label col-4">1. Person's name</label>
                                                <input class="form-control" style="width:12rem" id="userId" type="text"
                                                       placeholder="Enter name" />
                                            </div>
                                            <div class="form-group row my-3">
                                                <label class="col-form-label col-4">2. Select images</label>
                                                <input class="btn-light col form-control" style="width:8rem" id="faces" multiple="multiple" type="file" />
                                            </div>
                                            <div class="form-group row">
                                                <label class="col-form-label col-4">3. Final step</label>
                                                <input class="form-control btn-success" style="width:12rem;" id="register" type="button"
                                                       value="Register Faces" onclick="onRegisterFaces(userId.value, faces)" />
                                            </div>
                                        </div>

                                        <div class="mt-4 text-center">
                                            <input style="width:12rem" id="listfaces" type="button" class="btn  btn-primary ml-3``"
                                                   value="List Registered Faces" onclick="onListRegisteredFaces()" />
                                            <input style="width:12rem;" class="btn btn-danger" id="deleteFaces" type="button"
                                                   value="Delete Person's Images" onclick="onDeleteFace(userId.value)" />
                                        </div>

                                    </div>
                                </div>

                                <div class="card mt-3 d-none faceprocessing_queue">
                                    <div class="card-header h3">Face Recognition</div>
                                    <div class="card-body">

                                        <div class="form-group row">
                                            <label class="col-form-label col-2">Image</label>
                                            <input class="col form-control btn-light" id="faceInput" type="file" style="width:17rem"
                                                   onchange="return previewImage(this)" />
                                            <input class="form-control btn-success" type="button" value="Recognise" style="width:7rem"
                                                   id="recognise" onclick="onRecogniseFace(faceInput, minRecogConfidence.value)" />
                                        </div>

                                        <div class="form-group row mt-1">
                                            <label class="col-7 col-form-label text-end">Minimum Recognition Confidence</label>
                                            <input class="form-control" value="0.6" id="minRecogConfidence" type="text"
                                                   style="width:5rem" />
                                        </div>
                                    </div>
                                </div>

                            </div>

                            <div class="tab-pane fade show" id="text-panel" role="tabpanel" aria-labelledby="text-tab">

                                <div class="card mt-3 d-none textsummary_queue">
                                    <div class="card-header h3">Text Summary</div>
                                    <div class="card-body">

                                        <div class="form-group mt-1">
                                            <textarea id="articleText" class="w-100" style="height:200px"></textarea>
                                        </div>

                                        <div class="form-group row mt-1">
                                            <div class="col-2">
                                                <input class="btn-success col-form-label" type="button" value="Summarize"
                                                       id="summarize" onclick="onSummarizeText(articleText.value, numSentences.value)" />
                                            </div>

                                            <label class="col-5 col-form-label text-end">Sentences to produce</label>
                                            <input class="form-control" value="2" id="numSentences" type="number"
                                                   style="width:5rem" />
                                        </div>
                                    </div>

                                </div>

                                <div class="card mt-3 d-none sentimentanalysis_queue">
                                    <div class="card-header h3">Sentiment Analysis</div>
                                    <div class="card-body">

                                        <div class="form-group mt-1">
                                            <textarea id="sentimentText" class="w-100" style="height:200px"></textarea>
                                        </div>

                                        <div class="form-group row mt-1">
                                            <div class="col-2">
                                                <input class="btn-success col-form-label" type="button" value="Sentiment"
                                                       id="sentiment" onclick="onAnalyzeSentiment(sentimentText.value)" />
                                            </div>
                                        </div>
                                    </div>

                                </div>

                            </div>

                            <div class="tab-pane fade show" id="video-panel" role="tabpanel" aria-labelledby="video-tab">

                                <div class="card mt-3 d-none faceprocessing_queue">

                                    <div class="d-flex justify-content-start">
                                        <input class="btn btn-success" type="button" value="Start"
                                               onclick="onStartVideo()" style="width:5rem">
                                        <input class="btn btn-secondary " type="button" value="Stop"
                                               onclick="onStopVideo()" style="width:5rem">
                                    </div>
                                    <video id="video" style="width:100%;height:480px;display:none" autoplay></video>
                                    <canvas id="canvas"></canvas>
                                </div>
                            </div>

                            <div class="tab-pane fade show" id="benchmark-panel" role="tabpanel" aria-labelledby="benchmark-tab">

                                <div class="card mt-3 objectdetection_queue">
                                    <div class="card-header h3">Benchmark</div>
                                    <div class="card-body">

                                        <div class="form-group row">
                                            <label class="col-form-label col-2">Image</label>
                                            <input class="col form-control btn-light" id="benchmarkInput" type="file" style="width:17rem"
                                                   onchange="return previewImage(this)" />
                                        </div>
                                        <div class="form-group d-flex justify-content-end mt-2">
                                            <select id="benchmarkModel" class="form-select me-5">
                                                <option value="">Standard model</option>
                                                <option value="round-trip">No inference: round-trip speed test only</option>
                                                <option value="ipcam-general">General IP Cam</option>
                                                <option value="license-plate">License Plates v2</option>
                                                <option value="openlogo">Open Logo</option>
                                                <option value="actionnetv2">(Human) ActionNet v2</option>
                                            </select>
                                            <input class="form-control btn-success" type="button" value="Go" style="width:7rem"
                                                   id="recognise" onclick="onBenchmark(benchmarkInput, benchmarkModel.value, benchmarkResult)" />

                                            <!-- See note above on how this button isn't currently that useful
                                            <input type="button" value="Stop" class="styleD" style="margin-left:1em"
                                                                 onclick="isBenchmarkCancelled = true;" />
                                            -->
                                        </div>
                                        <div class="form-group row mt-1">
                                            <span id="benchmarkResult"></span>
                                        </div>
                                    </div>
                                </div>

                            </div>

                        </div>

                    </div>

                    <div class="ms-4 mt-3 position-sticky" style="width:475px;">
                        <div id="result" class="w-100 mb-4 bg-white border-1 border-light p-1 text-black"
                             style="height: 195px; overflow-y: auto;"></div>

                        <div class="w-100 position-relative border-1 border-light">

                            <canvas id="annotations" class="position-absolute"
                                    style="left:0;top:0;pointer-events:none;z-index:10"></canvas>

                            <div id="imageMask" class="position-absolute"
                                 style="left:0;top:0;pointer-events:none;z-index:10"></div>

                            <img src="" id="img" class="w-100" style="height:250px;visibility:hidden">
                        </div>
                    </div>

                </div>
            </form>
        </div>
    </div>

    <script src="assets/bootstrap.bundle.min.js"></script>
    <script>
        var triggerTabList = [].slice.call(document.querySelectorAll('#DemoTabs button'))
        triggerTabList.forEach(function (triggerEl) {
            var tabTrigger = new bootstrap.Tab(triggerEl)

            triggerEl.addEventListener('click', function (event) {
                event.preventDefault()
                tabTrigger.show()
            })
        })
    </script>

</body>
</html>